<!--
TODO:

2 views: frustum and NDC
-->

<html>

<head>
<title>Three.js example</title>
<style>

#ref-link { position : absolute; bottom : 0; left : 0; }

</style>

</head>

<body>
<!--script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script-->
<script src="https://cdn.jsdelivr.net/npm/three@0.115/build/three.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.115/examples/js/controls/OrbitControls.js"></script>

<div id="container">
    <canvas id="my_canvas"> </canvas>
</div>

<div id="ref-link">
<a href="https://github.com/Rabbid76/graphics-snippets/blob/master/documentation/model_view_projection_depth.md">
Model View Projection and Depth
</a>
</div>

<script>
(function onLoad() {
    let camera, renderer, scene, left_arr = [], right_arr = [], orbitControls;
    let eye = -8, frustum = {fov_y: Math.PI/6, aspect: 4/3, near: -4, far: 4 };
    let eye_offset = 0.5; 
    
    init();
    render();

    // init scene
    function init() {
        renderer = new THREE.WebGLRenderer({canvas: my_canvas, antialias: true, alpha: true});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.localClippingEnabled = true;

        camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 100);
        camera.position.set(0, 17, 5);
        camera.lookAt( 0, 0, 0 );

        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xffffff);
        
        window.onresize = () => {
            renderer.setSize(window.innerWidth, window.innerHeight);
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
        }
        
        scene.add(new THREE.AmbientLight(0x404040));
        scene.add((()=>{let l = new THREE.DirectionalLight( 0xffffff, 0.5 ); l.position.set(1,1,1.5); return l;})());
        scene.add((()=>{let l = new THREE.DirectionalLight( 0xffffff, 0.5 ); l.position.set(-1,1,1.5); return l;})());

        let left_grp = new THREE.Group();
        left_arr.push(create_camera(eye, 1));
        left_arr.push(...create_frustum(eye, frustum, 0xff0000, -0.01));
        left_grp.add(...left_arr);

        let right_grp = new THREE.Group();
        right_arr.push(create_camera(eye, 1));
        right_arr.push(...create_frustum(eye, frustum, 0x0000ff, 0.01));
        right_grp.add(...right_arr);

        scene.add(left_grp);
        scene.add(right_grp);
    }

    // create frustum
    function create_frustum(eye_x, frustum, color, fightoff) {
        
        let frustum_arr = [];
        let h_scale = Math.tan(frustum.fov_y/2)*2;
        let w_scale = h_scale * frustum.aspect;
        let hf = (frustum.far - eye_x) * h_scale, wf = (frustum.far - eye_x) * w_scale;
        let h0 = -eye_x * h_scale, w0 = -eye_x * w_scale;
        let hn = (frustum.near - eye_x) * h_scale, wn = (frustum.near - eye_x) * w_scale;
        
        // near plane
        frustum_arr.push( create_plane(frustum.near+fightoff, wn, hn, 0x00ffff, 0.3, 0.1) );
        // far plane
        frustum_arr.push( create_plane(frustum.far+fightoff, wf, hf, 0xff8000, 0.3, 0.1) );
        // focal plane
        frustum_arr.push( create_plane(fightoff, w0, h0, color, 1.0, 0.2) );

        // line of sight
        frustum_arr.push( new THREE.LineSegments( 
            new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye_x, 0, 0, frustum.far + 2, 0, 0]), 3) ),
            new THREE.LineBasicMaterial({color: color} ) ) );

        // pyramid
        frustum_arr.push( new THREE.LineSegments( 
            new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye_x, 0, 0, frustum.far, -hf/2, -wf/2, eye_x, 0, 0, frustum.far, hf/2, -wf/2,
                 eye_x, 0, 0, frustum.far, hf/2, wf/2, eye_x, 0, 0, frustum.far, -hf/2, wf/2 ]), 3) ),
            new THREE.LineBasicMaterial({color: color, opacity: 0.5, transparent: true} ) ) );

        return frustum_arr;
    }

    // create plane
    function create_plane(x, w, h, color, lopac, popac) {
        let plane_grp = new THREE.Group();
        let rect = (x, w, h) => { return new Float32Array([x, -w, -h, x, w, -h, x, w, h, x, -w, h]); }
        plane_grp.add( new THREE.Mesh( 
            new THREE.PlaneBufferGeometry( w, h, 1 ).rotateY(Math.PI / 2).translate(x, 0, 0),      
            new THREE.MeshBasicMaterial( {color: color, opacity: popac, transparent: popac < 1.0, side: THREE.DoubleSide} ) ) );
        plane_grp.add( new THREE.LineLoop( 
            new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute( rect(x, h/2, w/2), 3)),
            new THREE.LineBasicMaterial({color: color, opacity: lopac, transparent: lopac < 1.0,} ) ) );
        return plane_grp;
    }

    // camera
    function create_camera(eye_x, scale) {
        let camera_grp = new THREE.Group();
        let camera_mtl = new THREE.MeshPhongMaterial( {color: '#404040', side: THREE.DoubleSide} );

        camera_grp.add( new THREE.Mesh( 
            new THREE.ConeBufferGeometry(0.5*scale, scale, 32, 32, true).rotateZ(Math.PI / 2).translate(eye_x-scale/2, 0, 0),      
            camera_mtl) );
        camera_grp.add( new THREE.Mesh( 
            new THREE.CircleBufferGeometry(0.45*scale, 32).rotateY(Math.PI / 2).translate(eye_x-scale*0.1, 0, 0),      
            new THREE.MeshPhongMaterial( {color: '#8080ff', side: THREE.DoubleSide} ) ) ); 
        camera_grp.add(new THREE.Mesh( 
            ((w, h, d) => { return create_box(w, h, d, 0.2, 10).translate(eye_x-scale/2-w/2, 0, 0); })(1.5*scale, scale, scale),
             camera_mtl ));

        return camera_grp;
    }

    // create box with round edges
    function create_box( width, height, depth, radius0, smoothness ) {
        let shape = new THREE.Shape();
        let eps = 0.00001;
        let radius = radius0 - eps;
        shape.absarc( eps, eps, eps, -Math.PI / 2, -Math.PI, true );
        shape.absarc( eps, height -  radius * 2, eps, Math.PI, Math.PI / 2, true );
        shape.absarc( width - radius * 2, height -  radius * 2, eps, Math.PI / 2, 0, true );
        shape.absarc( width - radius * 2, eps, eps, 0, -Math.PI / 2, true );
        let geometry = new THREE.ExtrudeBufferGeometry( shape, {
            amount: depth - radius0 * 2,
            bevelEnabled: true,
            bevelSegments: smoothness * 2,
            steps: 1,
            bevelSize: radius,
            bevelThickness: radius0,
            curveSegments: smoothness
        });        
        geometry.center();
        return geometry;
    }

    // update objects and render scene
    function render(delta_t_ms) {
        requestAnimationFrame(render);
        orbitControls.update();
        
        if (delta_t_ms) {
            let h_scale = Math.tan(frustum.fov_y/2)*2;
            let w_scale = h_scale * frustum.aspect;
            let hf = (frustum.far - eye) * h_scale, wf = (frustum.far - eye) * w_scale;
            let eye_offs = (Math.sin(delta_t_ms / 1000) + 1);
            let near_offs = eye_offs * Math.abs(frustum.near / eye);
            let far_offs = -eye_offs * Math.abs(frustum.far / eye);
            let end_offs = -eye_offs * Math.abs((frustum.far+2) / eye);
            left_arr[0].position.z = -eye_offs;
            right_arr[0].position.z = eye_offs;
            left_arr[1].position.z = -near_offs;
            right_arr[1].position.z = near_offs;
            left_arr[2].position.z = -far_offs;
            right_arr[2].position.z = far_offs;
            left_arr[4].geometry.setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye, 0, -eye_offs, frustum.far + 2, 0, -end_offs]), 3) );
            right_arr[4].geometry.setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye, 0, eye_offs, frustum.far + 2, 0, end_offs]), 3) );
            left_arr[5].geometry.setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye, 0, -eye_offs, frustum.far, -hf/2, -wf/2-far_offs, eye, 0, -eye_offs, frustum.far, hf/2, -wf/2-far_offs,
                 eye, 0, -eye_offs, frustum.far, hf/2, wf/2-far_offs, eye, 0, -eye_offs, frustum.far, -hf/2, wf/2-far_offs ]), 3) );
            right_arr[5].geometry.setAttribute('position', new THREE.BufferAttribute( new Float32Array(
                [eye, 0, eye_offs, frustum.far, -hf/2, -wf/2+far_offs, eye, 0, eye_offs, frustum.far, hf/2, -wf/2+far_offs,
                 eye, 0, eye_offs, frustum.far, hf/2, wf/2+far_offs, eye, 0, eye_offs, frustum.far, -hf/2, wf/2+far_offs ]), 3) );
        }

        renderer.render(scene, camera);
    }
})();
</script>

</body>
</html>